/*
 * $Id: DefaultTreeTableModel.java 3780 2010-09-09 16:17:41Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.treetable;

import java.util.ArrayList;
import java.util.List;

import javax.swing.tree.TreePath;

/**
 * {@code DefaultTreeTableModel} is a concrete implementation of
 * {@code AbstractTreeTableModel} and is provided purely as a convenience for
 * use with {@code TreeTableNode}s. Applications that use {@code JXTreeTable}
 * without {@code TreeTableNode}s are expected to provide their own
 * implementation of a {@code TreeTableModel}.
 * <p>
 * The {@code DefaultTreeTableModel} is designed to be used with
 * {@code TreeTableNode}s. Specifically, users should extend
 * {@code AbstractMutableTreeTableNode} to provide custom implementations for
 * data display.
 * <p>
 * Users who do not provide a list of column identifiers must provide a root
 * that contains at least one column. Without specified identifiers the model
 * will attempt to calculate the columns required for display by querying the
 * root node. Normally, the root node can be little more than a shell (in
 * displays that hide it), but without identifiers, the model relies on the root
 * node metadata for display.
 * 
 * @author Ramesh Gupta
 * @author Karl Schaefer
 */
public class DefaultTreeTableModel extends AbstractTreeTableModel {
	/** The <code>List</code> of column identifiers. */
	protected List<?> columnIdentifiers;

	private boolean useAutoCalculatedIdentifiers;

	/**
	 * Creates a new {@code DefaultTreeTableModel} with a {@code null} root.
	 */
	public DefaultTreeTableModel() {
		this(null);
	}

	/**
	 * Creates a new {@code DefaultTreeTableModel} with the specified
	 * {@code root}.
	 * 
	 * @param root
	 *            the root node of the tree
	 */
	public DefaultTreeTableModel(TreeTableNode root) {
		this(root, null);
	}

	/**
	 * Creates a new {@code DefaultTreeTableModel} with the specified
	 * {@code root} and column names.
	 * 
	 * @param root
	 *            the root node of the tree
	 * @param columnNames
	 *            the names of the columns used by this model
	 * @see #setColumnIdentifiers(List)
	 */
	public DefaultTreeTableModel(TreeTableNode root, List<?> columnNames) {
		super(root);

		setColumnIdentifiers(columnNames);
	}

	private boolean isValidTreeTableNode(Object node) {
		boolean result = false;

		if (node instanceof TreeTableNode) {
			TreeTableNode ttn = (TreeTableNode) node;

			while (!result && ttn != null) {
				result = ttn == root;

				ttn = ttn.getParent();
			}
		}

		return result;
	}

	/**
	 * Replaces the column identifiers in the model. If the number of
	 * <code>newIdentifier</code>s is greater than the current number of
	 * columns, new columns are added to the end of each row in the model. If
	 * the number of <code>newIdentifier</code>s is less than the current number
	 * of columns, all the extra columns at the end of a row are discarded.
	 * <p>
	 * 
	 * @param columnIdentifiers
	 *            vector of column identifiers. If <code>null</code>, set the
	 *            model to zero columns
	 */
	// from DefaultTableModel
	public void setColumnIdentifiers(List<?> columnIdentifiers) {
		useAutoCalculatedIdentifiers = columnIdentifiers == null;

		this.columnIdentifiers = useAutoCalculatedIdentifiers ? getAutoCalculatedIdentifiers(getRoot()) : columnIdentifiers;

		modelSupport.fireNewRoot();
	}

	private static List<String> getAutoCalculatedIdentifiers(TreeTableNode exemplar) {
		List<String> autoCalculatedIndentifiers = new ArrayList<String>();

		if (exemplar != null) {
			for (int i = 0, len = exemplar.getColumnCount(); i < len; i++) {
				// forces getColumnName to use super.getColumnName
				autoCalculatedIndentifiers.add(null);
			}
		}

		return autoCalculatedIndentifiers;
	}

	/**
	 * Returns the root of the tree. Returns {@code null} only if the tree has
	 * no nodes.
	 * 
	 * @return the root of the tree
	 * 
	 * @throws ClassCastException
	 *             if {@code root} is not a {@code TreeTableNode}. Even though
	 *             subclasses have direct access to {@code root}, they should
	 *             avoid accessing it directly.
	 * @see AbstractTreeTableModel#root
	 * @see #setRoot(TreeTableNode)
	 */
	@Override
	public TreeTableNode getRoot() {
		return (TreeTableNode) root;
	}

	/**
	 * Gets the value for the {@code node} at {@code column}.
	 * 
	 * @impl delegates to {@code TreeTableNode.getValueAt(int)}
	 * @param node
	 *            the node whose value is to be queried
	 * @param column
	 *            the column whose value is to be queried
	 * @return the value Object at the specified cell
	 * @throws IllegalArgumentException
	 *             if {@code node} is not an instance of {@code TreeTableNode}
	 *             or is not managed by this model, or {@code column} is not a
	 *             valid column index
	 */
	@Override
	public Object getValueAt(Object node, int column) {
		if (!isValidTreeTableNode(node)) {
			throw new IllegalArgumentException("node must be a valid node managed by this model");
		}

		if (column < 0 || column >= getColumnCount()) {
			throw new IllegalArgumentException("column must be a valid index");
		}

		TreeTableNode ttn = (TreeTableNode) node;

		if (column >= ttn.getColumnCount()) {
			return null;
		}

		return ttn.getValueAt(column);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setValueAt(Object value, Object node, int column) {
		if (!isValidTreeTableNode(node)) {
			throw new IllegalArgumentException("node must be a valid node managed by this model");
		}

		if (column < 0 || column >= getColumnCount()) {
			throw new IllegalArgumentException("column must be a valid index");
		}

		TreeTableNode ttn = (TreeTableNode) node;

		if (column < ttn.getColumnCount()) {
			ttn.setValueAt(value, column);

			modelSupport.firePathChanged(new TreePath(getPathToRoot(ttn)));
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getColumnCount() {
		return columnIdentifiers.size();
	}

	/**
	 * {@inheritDoc}
	 */
	// Can we make getColumnClass final and avoid the complex DTM copy? -- kgs
	@Override
	public String getColumnName(int column) {
		// Copied from DefaultTableModel.
		Object id = null;

		// This test is to cover the case when
		// getColumnCount has been subclassed by mistake ...
		if (column < columnIdentifiers.size() && (column >= 0)) {
			id = columnIdentifiers.get(column);
		}

		return (id == null) ? super.getColumnName(column) : id.toString();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object getChild(Object parent, int index) {
		if (!isValidTreeTableNode(parent)) {
			throw new IllegalArgumentException("parent must be a TreeTableNode managed by this model");
		}

		return ((TreeTableNode) parent).getChildAt(index);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getChildCount(Object parent) {
		if (!isValidTreeTableNode(parent)) {
			throw new IllegalArgumentException("parent must be a TreeTableNode managed by this model");
		}

		return ((TreeTableNode) parent).getChildCount();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getIndexOfChild(Object parent, Object child) {
		if (!isValidTreeTableNode(parent) || !isValidTreeTableNode(child)) {
			return -1;
		}

		return ((TreeTableNode) parent).getIndex((TreeTableNode) child);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isCellEditable(Object node, int column) {
		if (!isValidTreeTableNode(node)) {
			throw new IllegalArgumentException("node must be a valid node managed by this model");
		}

		if (column < 0 || column >= getColumnCount()) {
			throw new IllegalArgumentException("column must be a valid index");
		}

		TreeTableNode ttn = (TreeTableNode) node;

		if (column >= ttn.getColumnCount()) {
			return false;
		}

		return ttn.isEditable(column);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isLeaf(Object node) {
		if (!isValidTreeTableNode(node)) {
			throw new IllegalArgumentException("node must be a TreeTableNode managed by this model");
		}

		return ((TreeTableNode) node).isLeaf();
	}

	/**
	 * Gets the path from the root to the specified node.
	 * 
	 * @param aNode
	 *            the node to query
	 * @return an array of {@code TreeTableNode}s, where
	 *         {@code arr[0].equals(getRoot())} and
	 *         {@code arr[arr.length - 1].equals(aNode)}, or an empty array if
	 *         the node is not found.
	 * @throws NullPointerException
	 *             if {@code aNode} is {@code null}
	 */
	public TreeTableNode[] getPathToRoot(TreeTableNode aNode) {
		List<TreeTableNode> path = new ArrayList<TreeTableNode>();
		TreeTableNode node = aNode;

		while (node != root) {
			path.add(0, node);

			node = node.getParent();
		}

		if (node == root) {
			path.add(0, node);
		}

		return path.toArray(new TreeTableNode[0]);
	}

	/**
	 * Sets the root for this table model. If no column identifiers have been
	 * specified, this will rebuild the identifier list, using {@code root} as
	 * an examplar of the table.
	 * 
	 * @param root
	 *            the node to set as root
	 */
	public void setRoot(TreeTableNode root) {
		this.root = root;

		if (useAutoCalculatedIdentifiers) {
			// rebuild the list
			// this already fires an event don't duplicate
			setColumnIdentifiers(null);
		} else {
			modelSupport.fireNewRoot();
		}
	}

	/**
	 * Invoked this to insert newChild at location index in parents children.
	 * This will then message nodesWereInserted to create the appropriate event.
	 * This is the preferred way to add children as it will create the
	 * appropriate event.
	 */
	public void insertNodeInto(MutableTreeTableNode newChild, MutableTreeTableNode parent, int index) {
		parent.insert(newChild, index);

		modelSupport.fireChildAdded(new TreePath(getPathToRoot(parent)), index, newChild);
	}

	/**
	 * Message this to remove node from its parent. This will message
	 * nodesWereRemoved to create the appropriate event. This is the preferred
	 * way to remove a node as it handles the event creation for you.
	 */
	public void removeNodeFromParent(MutableTreeTableNode node) {
		MutableTreeTableNode parent = (MutableTreeTableNode) node.getParent();

		if (parent == null) {
			throw new IllegalArgumentException("node does not have a parent.");
		}

		int index = parent.getIndex(node);
		node.removeFromParent();

		modelSupport.fireChildRemoved(new TreePath(getPathToRoot(parent)), index, node);
	}

	/**
	 * Called when value for the item identified by path has been changed. If
	 * newValue signifies a truly new value the model should post a
	 * {@code treeNodesChanged} event.
	 * <p>
	 * This changes the object backing the {@code TreeTableNode} described by
	 * the path. This change does not alter a nodes children in any way. If you
	 * need to change structure of the node, use one of the provided mutator
	 * methods.
	 * 
	 * @param path
	 *            path to the node that has changed
	 * @param newValue
	 *            the new value
	 * @throws NullPointerException
	 *             if {@code path} is {@code null}
	 * @throws IllegalArgumentException
	 *             if {@code path} is not a path managed by this model
	 * @throws ClassCastException
	 *             if {@code path.getLastPathComponent()} is not a
	 *             {@code TreeTableNode}
	 */
	@Override
	public void valueForPathChanged(TreePath path, Object newValue) {
		if (path.getPathComponent(0) != root) {
			throw new IllegalArgumentException("invalid path");
		}

		TreeTableNode node = (TreeTableNode) path.getLastPathComponent();
		node.setUserObject(newValue);

		modelSupport.firePathChanged(path);
	}

	/**
	 * Sets the user object for a node. Client code must use this method, so
	 * that the model can notify listeners that a change has occurred.
	 * <p>
	 * This method is a convenient cover for
	 * {@link #valueForPathChanged(TreePath, Object)}.
	 * 
	 * @param node
	 *            the node to modify
	 * @param userObject
	 *            the new user object to set
	 * @throws NullPointerException
	 *             if {@code node} is {@code null}
	 * @throws IllegalArgumentException
	 *             if {@code node} is not a node managed by this model
	 */
	public void setUserObject(TreeTableNode node, Object userObject) {
		valueForPathChanged(new TreePath(getPathToRoot(node)), userObject);
	}
}
